--------------Quiz Castle--------------
A 4am crack                  2018-08-31
---------------------------------------

Name: Quiz Castle
Genre: educational
Year: 1986
Credits: Jeffrey Jones
Publisher: Didatech Software
Platform: Apple ][+ or later
Media: 5.25-inch disk
Sides: 2
OS: Pronto-DOS
Previous cracks: none

                   ~

               Chapter 0
 In Which Various Automated Tools Fail
          In Interesting Ways


COPYA
  fails on first pass

Locksmith Fast Disk Backup
  can read every sector except T02,S07;
  copy displays title screen then quits
  to BASIC prompt with DOS disconnected

EDD 4 bit copy (no sync, no count)
  ditto

Copy ][+ nibble editor
  There's an address field for T02,S07,
  but no data

Disk Fixer
  T00 -> DOS 3.3-shaped RWTS
  T11 -> DOS 3.3 disk catalog
  T01,S07 -> startup program is "HELLO"

Why didn't COPYA work?
  intentionally unreadable sector on
  track $02

Why didn't Locksmith FDB / EDD work?
  probably a nibble check that checks
  that unreadable sector

Next steps:

  1. find runtime protection check
  2. disable it
  3. declare victory (*)

(*) go to the gym

                   ~

               Chapter 1
      In Which It's Better To Be
            Lucky Than Good


Since my copy goes down a different
code path than the original, I'm
guessing there is a runtime protection
check somewhere. One thing that all
protection checks have in common is
they turn on the drive motor by
accessing a specific address in the
$C0xx range. For slot 6, it's $C0E9,
but to allow disks to boot from any
slot, developers usually use code like
this:

  LDX <slot number x 16>
  LDA $C089,X

There's nothing that says where the
slot number has to be, although the
disk controller ROM routine uses zero
page $2B and lots of disks just reuse
that. There's also nothing that says
you have to use the X-register as the
index, or that you must use the
accumulator as the load register. But
most RWTS code does, out of convention
I suppose (or possibly fear of messing
up such low-level code in subtle ways).

Also, since developers don't actually
want people finding their protection-
related code, they may try to encrypt
it or obfuscate it to prevent people
from finding it. But eventually, the
code must exist and the code must run,
and it must run on my machine, and I
have the final say on what my machine
does or does not do.

But sometimes you get lucky.

Turning to my trusty Disk Fixer sector
editor, I search the non-working copy
for "BD 89 C0", which is the opcode
sequence for "LDA $C089,X".

[Disk Fixer]
  ["F"ind]
    ["H"ex]
      ["BD 89 C0"]

                 --v--

------------- DISK SEARCH -------------

$00/$07-$4F   $01/$0A-$46

                 --^--

The first match on track 0 is part of
the standard DOS 3.3 RWTS, i.e. not
suspicious. On an unprotected DOS 3.3
disk, that would be the only match, so
literally any other matches are
suspicious -- including this one on
track 1, which is doubly weird because
that's smack in the middle of DOS
itself.

Booting the disk and pressing <Ctrl-C>
gets me to a working prompt with DOS in
memory, so let's see it in its native
environment.

]PR#6
...
<Ctrl-C>

]CALL -151

Track 1, sector $0A would be loaded at
$AF00. Except this is Pronto-DOS, so
everything is shifted by 2 sectors. So
$AD00.

*AD46L

AD46-   BD 89 C0    LDA   $C089,X

There it is.

                   ~

               Chapter 2
     In Which All Bits Are Equal,
  But Some Are More Equal Than Others


From inspection, the protection routine
doesn't start at $AD46; it starts at
$ACEF. (Before that is an unconditional
JMP instruction.)

*ACEFL

; ($FE) -> $6000
ACEF-   A0 00       LDY   #$00
ACF1-   84 FE       STY   $FE
ACF3-   84 06       STY   $06
ACF5-   A9 60       LDA   #$60
ACF7-   85 FF       STA   $FF

; pass a byte to $ADC7
ACF9-   A9 BD       LDA   #$BD
ACFB-   20 C7 AD    JSR   $ADC7

*ADC7L

; ...which stores it in ($FE), which
; starts at $6000 and is incremented
; after each byte stored
ADC7-   91 FE       STA   ($FE),Y
ADC9-   E6 FE       INC   $FE
ADCB-   D0 02       BNE   $ADCF
ADCD-   E6 FF       INC   $FF
ADCF-   60          RTS

Ah! We're sneakily creating code, one
byte at a time.

Continuing from $ACFE...

; more sneaky code generation
ACFE-   A9 8C       LDA   #$8C
AD00-   20 C7 AD    JSR   $ADC7
AD03-   A9 C0       LDA   #$C0
AD05-   20 C7 AD    JSR   $ADC7
AD08-   A9 8D       LDA   #$8D
AD0A-   20 C7 AD    JSR   $ADC7
AD0D-   A9 C0       LDA   #$C0
AD0F-   18          CLC
AD10-   65 06       ADC   $06
AD12-   20 C7 AD    JSR   $ADC7
AD15-   A9 60       LDA   #$60
AD17-   20 C7 AD    JSR   $ADC7
AD1A-   E6 06       INC   $06
AD1C-   A5 06       LDA   $06
AD1E-   C9 1E       CMP   #$1E
AD20-   90 D7       BCC   $ACF9

; one final byte (looks like an "RTS")
AD22-   A9 60       LDA   #$60
AD24-   20 C7 AD    JSR   $ADC7

; get address of RWTS parameter table
AD27-   20 E3 03    JSR   $03E3

; ($08) -> RWTS parameter table
AD2A-   84 08       STY   $08
AD2C-   85 09       STA   $09

; track = $02
AD2E-   A9 02       LDA   #$02
AD30-   A0 04       LDY   #$04
AD32-   91 08       STA   ($08),Y

; command = $00 (seek)
AD34-   A9 00       LDA   #$00
AD36-   A0 0C       LDY   #$0C
AD38-   91 08       STA   ($08),Y

; volume = $00 (wildcard, matches any)
AD3A-   A0 03       LDY   #$03
AD3C-   91 08       STA   ($08),Y

; execute the command, seek to track 2
AD3E-   20 E3 03    JSR   $03E3
AD41-   20 D9 03    JSR   $03D9

; bail if that failed for some reason
AD44-   B0 27       BCS   $AD6D

; and we're back to the instruction
; that led us here in the first place,
; turning on the drive motor manually
AD46-   BD 89 C0    LDA   $C089,X

; initialize Death Counter
AD49-   A9 30       LDA   #$30
AD4B-   8D 78 05    STA   $0578
AD4E-   38          SEC
AD4F-   CE 78 05    DEC   $0578
AD52-   F0 19       BEQ   $AD6D

; find next address field
AD54-   20 44 B9    JSR   $B944
AD57-   B0 F5       BCS   $AD4E

; check sector number
AD59-   A5 2D       LDA   $2D

; we want logical sector 7, which is
; physical sector 1
AD5B-   C9 01       CMP   #$01
AD5D-   D0 EF       BNE   $AD4E

; reset data latch
AD5F-   BD 8E C0    LDA   $C08E,X

; wait
AD62-   A9 06       LDA   #$06
AD64-   20 A8 FC    JSR   $FCA8

; and call our mystery subroutine that
; we generated one byte at a time
AD67-   20 00 60    JSR   $6000

It's time to see what's at $6000. We
can run this code from the monitor as
long as we stop before doing any disk
stuff.

; stop after sneaky code generation and
; before doing any disk stuff
*AD27:60

; run just the code generation routine
*ACEFG

Now let's see what we built.

*6000L

6000-   BD 8C C0    LDA   $C08C,X
6003-   8D C0 60    STA   $60C0
6006-   BD 8C C0    LDA   $C08C,X
6009-   8D C1 60    STA   $60C1
600C-   BD 8C C0    LDA   $C08C,X
600F-   8D C2 60    STA   $60C2
6012-   BD 8C C0    LDA   $C08C,X
6015-   8D C3 60    STA   $60C3
6018-   BD 8C C0    LDA   $C08C,X
601B-   8D C4 60    STA   $60C4
601E-   BD 8C C0    LDA   $C08C,X
6021-   8D C5 60    STA   $60C5
6024-   BD 8C C0    LDA   $C08C,X
6027-   8D C6 60    STA   $60C6
602A-   BD 8C C0    LDA   $C08C,X
602D-   8D C7 60    STA   $60C7
6030-   BD 8C C0    LDA   $C08C,X
6033-   8D C8 60    STA   $60C8
6036-   BD 8C C0    LDA   $C08C,X
6039-   8D C9 60    STA   $60C9
603C-   BD 8C C0    LDA   $C08C,X
603F-   8D CA 60    STA   $60CA
6042-   BD 8C C0    LDA   $C08C,X
6045-   8D CB 60    STA   $60CB
6048-   BD 8C C0    LDA   $C08C,X
604B-   8D CC 60    STA   $60CC
604E-   BD 8C C0    LDA   $C08C,X
6051-   8D CD 60    STA   $60CD
6054-   BD 8C C0    LDA   $C08C,X
6057-   8D CE 60    STA   $60CE
605A-   BD 8C C0    LDA   $C08C,X
605D-   8D CF 60    STA   $60CF
6060-   BD 8C C0    LDA   $C08C,X
6063-   8D D0 60    STA   $60D0
6066-   BD 8C C0    LDA   $C08C,X
6069-   8D D1 60    STA   $60D1
606C-   BD 8C C0    LDA   $C08C,X
606F-   8D D2 60    STA   $60D2
6072-   BD 8C C0    LDA   $C08C,X
6075-   8D D3 60    STA   $60D3
6078-   BD 8C C0    LDA   $C08C,X
607B-   8D D4 60    STA   $60D4
607E-   BD 8C C0    LDA   $C08C,X
6081-   8D D5 60    STA   $60D5
6084-   BD 8C C0    LDA   $C08C,X
6087-   8D D6 60    STA   $60D6
608A-   BD 8C C0    LDA   $C08C,X
608D-   8D D7 60    STA   $60D7
6090-   BD 8C C0    LDA   $C08C,X
6093-   8D D8 60    STA   $60D8
6096-   BD 8C C0    LDA   $C08C,X
6099-   8D D9 60    STA   $60D9
609C-   BD 8C C0    LDA   $C08C,X
609F-   8D DA 60    STA   $60DA
60A2-   BD 8C C0    LDA   $C08C,X
60A5-   8D DB 60    STA   $60DB
60A8-   BD 8C C0    LDA   $C08C,X
60AB-   8D DC 60    STA   $60DC
60AE-   BD 8C C0    LDA   $C08C,X
60B1-   8D DD 60    STA   $60DD
60B4-   60          RTS

Normal RWTS code reads a nibble from
disk in a tight loop, like this:

   @   LDA   $C08C,X
       BPL   @

This waits for the high bit to be set,
which signifies that the entire nibble
has "entered" the data latch and is
complete. (All valid nibbles have the
high bit set.)

This code, on the other hand, has no
BPL loops. This will just keep reading
the data latch as fast as possible and
storing the raw partial nibble values
in $60C0..$60DD. So this will capture
all sorts of "intermediate" values, the
ones that a normal RWTS would discard
because they weren't a complete nibble
value yet.

This is as close to a raw bitstream as
you can get. You don't generally see
code like this. It's not useful for
reading data, because it doesn't wait
for a complete nibble. And it's not
practical for bit copiers, because it
only captures a small section of the
bitstream on the track -- 30 bits out
of about 50,000.

But I bet these 30 bits are really
important.

                   ~

               Chapter 3
     In Which Our Adventure Takes
      An Unexpectedly Nasty Turn


Continuing from $AD6A, after returning
from the raw bit reading code at $6000:

; unconditional jump
AD6A-   18          CLC
AD6B-   90 04       BCC   $AD71

; failure ends up here (from $AD44) --
; get the RWTS error code
AD6D-   A0 0D       LDY   #$0D
AD6F-   B1 08       LDA   ($08),Y

; execution continues here regardless,
; and we turn off the drive motor
AD71-   9D 88 C0    STA   $C088,X

; reset zero page after RWTS call
AD74-   A0 00       LDY   #$00
AD76-   84 48       STY   $48

; branch forward on previous failure
; (success path will still have the
; carry bit clear)
AD78-   B0 38       BCS   $ADB2

; initialize Death Counter
AD7A-   84 FE       STY   $FE

; get an address from a lookup table
; that's part of this protection
; routine
AD7C-   A2 00       LDX   #$00
AD7E-   84 06       STY   $06
AD80-   BD D0 AD    LDA   $ADD0,X
AD83-   85 08       STA   $08
AD85-   BD D5 AD    LDA   $ADD5,X
AD88-   85 09       STA   $09

The addresses at $ADD0 and $ADD5 point
to different sequences at $ADDA, $ADE3,
and a few others. Here's one of them:

*ADDA.

ADD8- .. .. 05 08 0A 0B 10 14
ADE0- 15 16 FF .. .. .. .. ..

Then we scan through that sequence
(each sequence is terminated with an
$FF byte) and look for an exact match
within the raw data latch values we
captured at $6000 and stored at $60C0:

AD8A-   A0 00       LDY   #$00
AD8C-   B1 08       LDA   ($08),Y
AD8E-   C9 FF       CMP   #$FF
AD90-   D0 0A       BNE   $AD9C

; went through this entire sequence
; without a match, so increment Death
; Counter and start over with the first
; match sequence again
AD92-   E6 FE       INC   $FE
AD94-   A4 FE       LDY   $FE
AD96-   C0 1A       CPY   #$1A
AD98-   90 E2       BCC   $AD7C

; protection check failed, branch to
; failure path
AD9A-   B0 16       BCS   $ADB2

; Check the partial nibble values that
; were stored by the routine at $6000.
; Each time through the loop, we
; examine a specific raw nibble value.
; So I guess we're not looking for an
; entire sequence of partial nibble
; values? Just whether the first one is
; in any of the tables of acceptable
; values, then the second, then the
; third, &c.
AD9C-   84 07       STY   $07
AD9E-   A4 06       LDY   $06
ADA0-   D9 C0 60    CMP   $60C0,Y

; found a match for this partial nibble
; value, so branch forward to continue
ADA3-   F0 05       BEQ   $ADAA

; otherwise keep looking for a match
ADA5-   A4 07       LDY   $07
ADA7-   C8          INY
ADA8-   D0 E2       BNE   $AD8C

; execution continues here regardless,
; possibly from $ADA3 or by falling
; through --
; try the next sequence (there are 5)
ADAA-   E6 06       INC   $06
ADAC-   E8          INX
ADAD-   E0 05       CPX   #$05
ADAF-   90 CF       BCC   $AD80

; protection routine passed, we matched
; enough partial nibble values --
; clear carry bit and continue
ADB1-   18          CLC

; execution continues here regardless
; of whether the protection passed (we
; also end up here from $AD9A if the
; protection failed, but with the carry
; bit set)
ADB2-   A0 FE       LDY   #$FE
ADB4-   90 01       BCC   $ADB7

; only executed if the protection check
; failed
ADB6-   C8          INY
ADB7-   8C FF B7    STY   $B7FF

OK, so there's the difference between
an original disk and a copy: the value
of the Y register at $ADB7, which gets
stored in $B7FF.

original = #$FE
copy     = #$FF

; cover our tracks by overwriting the
; JMP instruction that sent us here,
; then continue with loading DOS
ADBA-   A9 80       LDA   #$80
ADBC-   8D 4E 9E    STA   $9E4E
ADBF-   A9 A1       LDA   #$A1
ADC1-   8D 4F 9E    STA   $9E4F
ADC4-   4C 80 A1    JMP   $A180

I didn't realize it until just now, but
that explains how and when this routine
is called. At the end of loading DOS,
it normally jumps to $A180 to load and
execute the HELLO program. Based on how
the protection check exits, I went back
and looked at that part of the DOS, and
this disk jumps to $ACEF instead, to do
all this and set what I assume is a
very important side effect in $B7FF.

Either way, the protection check exits
via the standard $A180 DOS routine.
The only difference is the value in
$B7FF.

That's unexpectedly nasty. I love it,

Returning to my trusty sector editor, I
searched the disk for "FF B7" to see
where this address is accessed later.

                 --v--

------------- DISK SEARCH -------------

$01/$0A-$B8   $08/$0A-$DB   $08/$0A-$E2

                 --^--

Track 1 is the protection check itself,
where it sets $B7FF. Track 8 must be
where it's read later.

                 --v--

T08,S0A
----------- DISASSEMBLY MODE ----------
00D3:AD DF BC       LDA   $BCDF
00D6:C9 FE          CMP   #$FE
00D8:D0 05          BNE   $00DF
00DA:4D FF B7       EOR   $B7FF   <-- !
00DD:F0 0E          BEQ   $00ED
00DF:A9 00          LDA   #$00
00E1:8D FF B7       STA   $B7FF
00E4:20 58 FC       JSR   $FC58
00E7:20 99 F3       JSR   $F399
00EA:4C 9B FA       JMP   $FA9B

                 --^--

Aha! The carry is set, so Y is
incremented, so $B7FF ends up with #$FF
instead of #$FE, so MUCH MUCH LATER
this EOR doesn't produce zero, so we
fall into The Badlands (at offset
$00DF) that clears the screen and exits
to a BASIC prompt.

Which is exactly the behavior I saw on
my non-working copy.

                   ~

               Chapter 4
      One Byte To Rule Them All,
     And In The Darkness Bind Them


To make my non-working copy work like
the original disk, I can change a
single instruction within the
protection check:

ADB6-   C8          INY

into

ADB6-   EA          NOP

So even when the protection fails, the
value at $B7FF will be correct.

T01,S0A,$B6: C8 -> EA

]PR#6
...works...

Side B has identical protection.

Quod erat liberandum.

---------------------------------------
A 4am crack                    No. 2091
------------------EOF------------------
